(https://segmentfault.com/a/1190000012431988)
3****
以下内容均是笔者学习过程中收集的知识点,顺序比较跳跃,初衷是为了方便查阅,顺便加深记忆。内容会不断更新,如果有什么问题或者有好的 Swift 方面的语法糖或者知识点也可以提出来,我会挑选斟酌后收录,欢迎大家关注~
环境:
Swift 4.0
Xcode 9.1
Associated Object
Objective-C 的 runtime 里的 Associated Object 允许我们在使用 Category 扩展现有的类的功能的时候,直接添加实例变量。在 Swift 中 extension 不能添加存储属性,我们可以利用 Associated Object 来实现,比如下面的 title
「实际上」是一个存储属性:
1 | // MyClass.swift |
1 | // 测试 |
Delegate 声明为 weak
Swift 中 Delegate 需要被声明成 weak
,来避免访问到已被回收的内存而导致崩溃,如果我们像下面这样,是编译不过的:
1 | protocol MyClassDelegate { |
这是因为 Swift 的 protocol 是可以被除了 class 以外的其他类型遵守的,而对于像 struct
或是 enum
这样的类型,本身就不通过引用计数来管理内存,所以也不可能用 weak
这样的 ARC 的概念来进行修饰。
想要在 Swift 中使用 weak delegate,我们就需要将 protocol 限制在 class 内:
- 一种做法是将 protocol 声明为 Objective-C 的,这可以通过在 protocol 前面加上
@objc
关键字来达到,Objective-C 的 protocol 都只有类能实现,因此使用 weak 来修饰就合理了:
1 | @objc protocol MyClassDelegate { |
- 另一种可能更好的办法是在 protocol 声明的名字后面加上
class
,这可以为编译器显式地指明这个 protocol 只能由class
来实现,避免了过多的不必要的 Objective-C 兼容:
1 | protocol MyClassDelegate: class { |
可选协议和协议扩展
Objective-C 中的 protocol 里存在 @optional
关键字,被这个关键字修饰的方法并非必须要被实现,原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的,如果不是实现是无法编译的:
1 | class ViewController: UIViewController,MyProtocol { } |
如果我们想要像 Objective-C 里那样定义可选的协议方法,就需要将协议本身和可选方法都定义为 Objective-C 的,也即在 protocol 定义之前加上 @objc
,方法之前加上 @objc optional
:
1 | @objc protocol MyProtocol { |
另外,对于所有的声明,它们的前缀修饰是完全分开的,也就是说你不能像是在 Objective-C 里那样用一个 @optional 指定接下来的若干个方法都是可选的了,必须对每一个可选方法添加前缀,对于没有前缀的方法来说,它们是默认必须实现的:
1 | @objc protocol MyProtocol { |
一个不可避免的限制是,使用 @objc
修饰的 protocol 就只能被 class
实现了,也就是说,对于 struct
和 enum
类型,我们是无法令它们所实现的协议中含有可选方法或者属性的。另外,实现它的 class
中的方法还必须也被标注为 @objc
,或者整个类就是继承自 NSObject
。对于这种问题,在 Swift 2.0 中,我们有了另一种选择,那就是使用 protocol extension。我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现,这样这些方法在实际的类中就是可选实现的了:
1 | protocol MyProtocol { |
1 | class ViewController: UIViewController,MyProtocol { |
单例
Swift 中的单例非常简单,Swift 1.2 以及之后:
1 | class Singleton { |
这种写法不但是线程安全的,也是懒加载的,let
定义的属性本身就是线程安全的,同时 static
定义的是一个 class constant,拥有全局作用域和懒加载特性。
另外,这个类型中加入了一个私有的初始化方法,来覆盖默认的公开初始化方法,这让项目中的其他地方不能够通过 init 来生成自己的 Singleton
实例,也保证了类型单例的唯一性。如果你需要的是类似 default 的形式的单例 (也就是说这个类的使用者可以创建自己的实例) 的话,可以去掉这个私有的 init
方法。
输出格式化
在 Objective-C 中的 %@
这样的格式在指定的位置设定占位符,然后通过参数的方式将实际要输出的内容补充完整。例如 Objective-C 中常用的向控制台输出的 NSLog
方法就使用了这种格式化方法:
1 | float a = 1.234567; |
对应 Swift 中我们可以这样:
1 | let a = 1.234567 |
Selector
@selector
是 Objective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL 类型,它的表现很类似一个动态的函数指针。在 Swift 中没有 @selector
了,取而代之,从 Swift 2.2 开始我们使用 #selector
来从暴露给 Objective-C 的代码中获取一个 selector
,并且因为 selector
是 Objective-C runtime 的概念,在 Swift 4 中,默认情况下所有的 Swift 方法在 Objective-C 中都是不可见的,所以你需要在这类方法前面加上 @objc
关键字,将这个方法暴露给 Objective-C,才能进行使用:
1 | let btn = UIButton.init(type: .system) |
1 | ... |
将 protocol 的方法声明为 mutating
Swift 的 protocol 不仅可以被 class 类型实现,也适用于 struct
和 enum
,因为这个原因,我们在写给别人用的协议时需要多考虑是否使用 mutating
来修饰方法。Swift 的 mutating
关键字修饰方法是为了能在该方法中修改 struct
或是 enum
的变量,所以如果你没在协议方法里写 mutating
的话,别人如果用 struct
或者 enum
来实现这个协议的话,就不能在方法里改变自己的变量了,比如下面的代码是编译不过的:
1 | protocol Vehicle { |
我们应该加上 mutating
关键字:
1 | protocol Vehicle { |
数组遍历 enumerate
使用 NSArray 时一个很常遇见的的需求是在枚举数组内元素的同时也想使用对应的下标索引,在 Objective-C 中最方便的方式是使用 NSArray 的 enumerateObjectsUsingBlock:
,在 Swift 中存在一个效率,安全性和可读性都很好的替代,那就是快速枚举某个数组的EnumerateGenerator
,它的元素是同时包含了元素下标索引以及元素本身的多元组:
1 | let arr = ["a","b","c","d","e"] |
输入输出参数 inout
函数参数默认是常量,如果试图在函数体中更改参数值将会导致编译错误,比如下面的例子中尝试着交换值:
1 | func swapTwoInts(_ a: Int, _ b: Int) { |
如果想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters):
1 | func swapTwoInts(_ a: inout Int, _ b: inout Int) { |
Default 参数
Swift 的方法是支持默认参数的,也就是说在声明方法时,可以给某个参数指定一个默认使用的值。在调用该方法时要是传入了这个参数,则使用传入的值,如果缺少这个输入参数,那么直接使用设定的默认值进行调用。和其他很多语言的默认参数相比较,Swift 中的默认参数限制更少,并没有所谓 “默认参数之后不能再出现无默认值的参数”这样的规则,举个例子,下面两种方法的声明在 Swift 里都是合法可用的:
1 | func sayHello1(str1: String = "Hello", str2: String, str3: String) { |
1 | sayHello1(str2: " ", str3: "World") |
延迟加载 lazy
延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有一些对象的属性和内容非常复杂的话,这个时间更是不可忽略。另外,有些情况下我们并不会立即用到一个对象的所有属性,而默认情况下初始化时,那些在特定环境下不被使用的存储属性,也一样要被初始化和赋值,也是一种浪费。在 Objective-C 中,一个延迟加载一般是这样的:
1 | // ClassA.h |
对应在 Swift 中,使用 lazy
作为属性修饰符时,只能声明属性是变量,且我们需要显式地指定属性类型,否则会编译错误:
1 | class ClassA { |
我们应该声明为 var
并指定好类型:
1 | class ClassA { |
如果不需要做什么额外工作的话,也可以对这个 lazy
的属性直接写赋值语句:
1 | lazy var str: String = "Hello" |
我们还可以利用 lazy
配合像 map
或是 filter
这类接受闭包并进行运行的方法一起,让整个行为变成延时进行的。在某些情况下这么做也对性能会有不小的帮助。例如,直接使用 map 时:
1 | let data = 1...3 |
而如果我们先进行一次 lazy
操作的话,我们就能得到延时运行版本的容器:
1 | let data = 1...3 |
对于那些不需要完全运行,可能提前退出的情况,使用 lazy 来进行性能优化效果会非常有效。
编译标记
在 Objective-C 中,我们经常在代码中插入 #param
符号来标记代码的区间,这样在 Xcode 的导航栏中我们就可以看到组织分块后的方法列表。在 Swift 中我们可以用 MARK:
来代替:
在 Objective-C 中还有一个很常用的编译标记,那就是 #warning
,一个 #warning
标记可以在 Xcode 的代码编辑器中显示为明显的黄色警告条,非常适合用来提示代码的维护者和使用者需要对某些东西加以关注。在 Swift 中我们可以用 FIXME:
和 TODO:
配合 shell
来代替:
脚本:
1 | TAGS="TODO:|FIXME:" |
效果:
换行符
在 Swift 3 中,需要换行时是需要 \n
:
1 | let str = "xxxx\nxxx" |
在 swift 4 中,我们可以使用 """
:
1 | let jsonStr = """ |
字符串切割 split
我们需要切割某个字符串时可以用 split
方法,需要注意的是,返回的结果是个数组:
1 | let str = "Hello,world !" |
KVC
1 | class MyClass { |
Swift 4 中 Apple 引入了新的 KeyPath 的表达方式,现在,对于类型 MyClass
中的变量 name
,对应的 KeyPath 可以写为 \MyClass.name
,利用 KVC 修改 name
值的话,我们可以这么操作:
1 | let object = MyClass() |
另外 Swift 4 中 struct
同样支持 KVC :
1 | struct MyStruct { |
Swift 中值类型和引用类型注意点
KVC 一节中代码里有个注意点:
1 | var obj = MyStruct(age: 18) |
是编译不过的,会报错:
1 | Cannot assign to immutable expression of type 'Int' |
笔者初次也犯了这样的错误,想当然的认为 MyClass
用 let
声明的是没有问题的,struct
也一样:
1 | let object = MyClass() |
其实原因很简单,swift 中 Class 是引用类型的,而 struct 是值类型的:值类型在被赋给一个变量,或被传给函数时,实际是做了一次拷贝。引用类型在被赋给一个变量,或被传给函数时,传递的是引用。
KVO
很遗憾,依然只有 NSObject
才能支持 KVO,另外由于 Swift 为了效率,默认禁用了动态派发,因此想用 Swift 来实现 KVO,我们还需要做额外的工作,那就是将想要观测的对象标记为 dynamic
和 @objc
,下面的 ? 是 ViewController
监听 MyClass
的 date
属性:
1 | class MyClass: NSObject { |
在 Objective-C 中我们几乎可以没有限制地对所有满足 KVC 的属性进行监听,而现在我们需要属性有 dynamic 和 @objc进行修饰。大多数情况下,我们想要观察的类包含这两个修饰 (除非这个类的开发者有意为之,否则一般也不会有人愿意多花功夫在属性前加上它们,因为这毕竟要损失一部分性能),并且有时候我们很可能也无法修改想要观察的类的源码。遇到这样的情况的话,一个可能可行的方案是继承这个类并且将需要观察的属性使用 dynamic 和 @objc 进行重写。比如刚才我们的 MyClass
中如果 date
没有相应标注的话,我们可能就需要一个新的 MyChildClass
了:
1 | class MyClass: NSObject { |
Swift UIButton 状态的叠加
在 Objective-C 中,如果我们想叠加按钮的某个状态,可以这么写:
1 | UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom]; |
对应的 Swift 我们可以这么写:
1 | let btn = UIButton.init(type: .custom) |
把需要叠加的状态用个数组装起来就行了。
Swift 中的 “@synchronized”
在 Objective-C 中,我们可以用 @synchronized
这个关键字可以用来修饰一个变量,并为其自动加上和解除互斥锁。这样,可以保证变量在作用范围内不会被其他线程改变:
1 | - (void)myMethod:(id)anObj { |
虽然这个方法很简单好用,但是很不幸的是在 Swift 中它已经 (或者是暂时) 不存在了。其实 @synchronized
在幕后做的事情是调用了 objc_sync
中的 objc_sync_enter
和 objc_sync_exit
方法,并且加入了一些异常判断。因此,在 Swift 中,如果我们忽略掉那些异常的话,我们想要 lock 一个变量的话,可以这样写:
1 | func myMethod(anObj: AnyObject!) { |
更进一步,如果我们喜欢以前的那种形式,甚至可以写一个全局的方法,并接受一个闭包,来将 objc_sync_enter
和 objc_sync_exit
封装起来:
1 | func synchronized(_ lock: AnyObject, closure: () -> ()) { |
这样使用起来就和 Objective-C 中 @synchronized
很像了。
再举个 ? ,如果我们想为某个类实现一个线程安全的 setter
,可以这样:
1 | class Obj { |
自定义日志输出
在 Objective-C 中,我们通常会自定义日志输出来完善信息以及避免 release
下的输出,比如下面这种,可以额外提供行数、方法名等信息:
1 | #ifdef DEBUG |
1 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
在 Swift 中,我们可以这样自定义:
1 | func xxprint<T>(_ message: T, filePath: String = #file, line: Int = #line, function: String = #function) { |
1 | class ViewController: UIViewController { |
Swift 中的 “readonly”
在 Objective-C 中,我们通常把属性声明为 readonly
来提醒别人:“不要修改!!”,通常这么写:
1 | @interface Person : NSObject |
如果外部尝试修改的话,会编译错误:
1 | - (void)viewDidLoad { |
有些情况下,我们希望内部可以点语法访问 name
属性,也就是 self.name
,但是因为是 readonly
的,会编译错误:
1 | @implementation Person |
这时候我们就会在内部的 extension
重新声明一个 readwrite
的同样的属性,也就是“外部只读,内部可写”:
1 | @interface Person () |
在 Swift 中,我们可能有同样的场景。这里就不得不提到 private
和 fileprivate
关键字了。private
表示声明为私有的实体只能在其声明的范围内被访问。比如我在 MyClass
中声明了一个私有的 name
属性,外部访问的话会编译错误:
1 | class MyClass { |
而 fileprivate
,看命名我们大概能猜到,就是将对实体的访问权限于它声明的源文件。通俗点讲,比如我上面的代码都是在 ViewController.swift
这个文件里的,我把 private
修改为 fileprivate
,就不会编译错误了:
1 | class MyClass { |
那么如果非 ViewController.swift
文件,也想访问 MyClass
的 name
属性该怎么办呢?我们可以把 name
属性声明为 fileprivate(set)
,就要就达到类似 Objective-C 中的 readonly
效果了 :
1 | // ViewController.swift 文件 |
1 | // AppDelegate.swift 文件 |
作用域:do 语句块
在 Objective-C 中,我们可以利用 {}
来开辟新的作用域,来避免对象名称重复的问题:
1 | NSString *ha = @"测试一"; |
在 Swift 中,取代 {}
的是 do {}
:
1 | let ha = "测试一" |
倒序 reversed()
在 Objective-C 中,我们如果想倒序数组一般这样:
1 | NSArray *array = @[@"1",@"2",@"3"]; |
在 Swift 中,相对简单点:
1 | let arr:[String] = ["1","2","3"] |
标签语句:指定跳出某个条件语句
在 Objective-C 中,如果遇到多层嵌套的条件语句,我们如果想要指定跳出某个条件语句是很不方便的。比如有两个循环,一旦找到它们相同的,就立刻停止循环,我们可能会这么做:
1 | NSArray *arr1 = @[@"1",@"2",@"3",@"4",@"5"]; |
我们需要借助 finded
这个 BOOL
,来方便我们跳出循环。在 Swift 中,我们就可以利用标签语句,来指定具体跳出哪个循环,语法是这样的:
1 | 标签名: 条件语句 { |
上面的 ? 我们可以这么写:
1 | let arr1 = ["1","2","3","4","5"] |
上面代码,我们把第一层循环定义了标签:label
。在第二层循环中,一旦条件成立,立刻跳出第一层循环 label
。这个特性,可以说十分方便了!
优雅的定义通知名称
在 Objective-C 中,我们自定义通知时,对于名称的定义一般都有规范:
1 | // xxx.h |
在 Swift 中,我们可以参考 Alamofire 的方式,创建个专门存放通知名的文件,扩展 Notification.Name
并以结构体 struct
方式声明:
1 | // XXNotification.swift 文件 |
然后我们就可以愉快的使用了:
1 | // add |